import Tone from "../core/Tone";
import "../instrument/Instrument";
import "../source/FMOscillator";
import "../component/Filter";
import "../component/FrequencyEnvelope";
import "../component/AmplitudeEnvelope";
import "../core/Gain";
import "../signal/Scale";
import "../signal/Multiply";

/**
 *  Inharmonic ratio of frequencies based on the Roland TR-808
 *  Taken from https://ccrma.stanford.edu/papers/tr-808-cymbal-physically-informed-circuit-bendable-digital-model
 *  @private
 *  @static
 *  @type {Array}
 */
var inharmRatios = [1.0, 1.483, 1.932, 2.546, 2.630, 3.897];

/**
 *  @class  A highly inharmonic and spectrally complex source with a highpass filter
 *          and amplitude envelope which is good for making metalophone sounds. Based
 *          on CymbalSynth by [@polyrhythmatic](https://github.com/polyrhythmatic).
 *          Inspiration from [Sound on Sound](https://web.archive.org/web/20160610143924/https://www.soundonsound.com/sos/jul02/articles/synthsecrets0702.asp).
 *
 *  @constructor
 *  @extends {Tone.Instrument}
 *  @param {Object} [options] The options availble for the synth
 *                             see defaults below
 */
Tone.MetalSynth = function(options){

	options = Tone.defaultArg(options, Tone.MetalSynth.defaults);
	Tone.Instrument.call(this, options);

	/**
	 *  The frequency of the cymbal
	 *  @type  {Frequency}
	 *  @signal
	 */
	this.frequency = new Tone.Signal(options.frequency, Tone.Type.Frequency);

	/**
	 *  The array of FMOscillators
	 *  @type  {Array}
	 *  @private
	 */
	this._oscillators = [];

	/**
	 *  The frequency multipliers
	 *  @type {Array}
	 *  @private
	 */
	this._freqMultipliers = [];

	/**
	 *  The amplitude for the body
	 *  @type {Tone.Gain}
	 *  @private
	 */
	this._amplitue = new Tone.Gain(0).connect(this.output);

	/**
	 *  highpass the output
	 *  @type {Tone.Filter}
	 *  @private
	 */
	this._highpass = new Tone.Filter({
		"type" : "highpass",
		"Q" : -3.0102999566398125
	}).connect(this._amplitue);

	/**
	 *  The number of octaves the highpass
	 *  filter frequency ramps
	 *  @type {Number}
	 *  @private
	 */
	this._octaves = options.octaves;

	/**
	 *  Scale the body envelope
	 *  for the bandpass
	 *  @type {Tone.Scale}
	 *  @private
	 */
	this._filterFreqScaler = new Tone.Scale(options.resonance, 7000);

	/**
	 *  The envelope which is connected both to the
	 *  amplitude and highpass filter's cutoff frequency
	 *  @type  {Tone.Envelope}
	 */
	this.envelope = new Tone.Envelope({
		"attack" : options.envelope.attack,
		"attackCurve" : "linear",
		"decay" : options.envelope.decay,
		"sustain" : 0,
		"release" : options.envelope.release,
	}).chain(this._filterFreqScaler, this._highpass.frequency);
	this.envelope.connect(this._amplitue.gain);

	for (var i = 0; i < inharmRatios.length; i++){
		var osc = new Tone.FMOscillator({
			"type" : "square",
			"modulationType" : "square",
			"harmonicity" : options.harmonicity,
			"modulationIndex" : options.modulationIndex
		});
		osc.connect(this._highpass);
		this._oscillators[i] = osc;

		var mult = new Tone.Multiply(inharmRatios[i]);
		this._freqMultipliers[i] = mult;
		this.frequency.chain(mult, osc.frequency);
	}

	//set the octaves
	this.octaves = options.octaves;

};

Tone.extend(Tone.MetalSynth, Tone.Instrument);

/**
 *  default values
 *  @static
 *  @const
 *  @type {Object}
 */
Tone.MetalSynth.defaults = {
	"frequency" : 200,
	"envelope" : {
		"attack" : 0.001,
		"decay" : 1.4,
		"release" : 0.2
	},
	"harmonicity" : 5.1,
	"modulationIndex" : 32,
	"resonance" : 4000,
	"octaves" : 1.5
};

/**
 *  Trigger the attack.
 *  @param  {Time}  time      When the attack should be triggered.
 *  @param  {NormalRange}  [velocity=1]  The velocity that the envelope should be triggered at.
 *  @return  {Tone.MetalSynth}  this
 */
Tone.MetalSynth.prototype.triggerAttack = function(time, vel){
	time = this.toSeconds(time);
	vel = Tone.defaultArg(vel, 1);
	this.envelope.triggerAttack(time, vel);
	this._oscillators.forEach(function(osc){
		osc.start(time);
	});
	//if the sustain is 0, stop the oscillator as well
	if (this.envelope.sustain === 0){
		this._oscillators.forEach(function(osc){
			osc.stop(time + this.envelope.attack + this.envelope.decay);
		}.bind(this));
	}
	return this;
};

/**
 *  Trigger the release of the envelope.
 *  @param  {Time}  time      When the release should be triggered.
 *  @return  {Tone.MetalSynth}  this
 */
Tone.MetalSynth.prototype.triggerRelease = function(time){
	time = this.toSeconds(time);
	this.envelope.triggerRelease(time);
	this._oscillators.forEach(function(osc){
		osc.stop(time + this.envelope.release);
	}.bind(this));
	return this;
};

/**
 * Sync the instrument to the Transport. All subsequent calls of
 * [triggerAttack](#triggerattack) and [triggerRelease](#triggerrelease)
 * will be scheduled along the transport.
 * @example
 * synth.sync()
 * //schedule 3 notes when the transport first starts
 * synth.triggerAttackRelease('8n', 0)
 * synth.triggerAttackRelease('8n', '8n')
 * synth.triggerAttackRelease('8n', '4n')
 * //start the transport to hear the notes
 * Transport.start()
 * @returns {Tone.Instrument} this
 */
Tone.MetalSynth.prototype.sync = function(){
	this._syncMethod("triggerAttack", 0);
	this._syncMethod("triggerRelease", 0);
	return this;
};

/**
 *  Trigger the attack and release of the envelope after the given
 *  duration.
 *  @param  {Time}  duration  The duration before triggering the release
 *  @param  {Time}  time      When the attack should be triggered.
 *  @param  {NormalRange}  [velocity=1]  The velocity that the envelope should be triggered at.
 *  @return  {Tone.MetalSynth}  this
 */
Tone.MetalSynth.prototype.triggerAttackRelease = function(duration, time, velocity){
	time = this.toSeconds(time);
	duration = this.toSeconds(duration);
	this.triggerAttack(time, velocity);
	this.triggerRelease(time + duration);
	return this;
};

/**
 *  The modulationIndex of the oscillators which make up the source.
 *  see Tone.FMOscillator.modulationIndex
 *  @memberOf Tone.MetalSynth#
 *  @type {Positive}
 *  @name  modulationIndex
 */
Object.defineProperty(Tone.MetalSynth.prototype, "modulationIndex", {
	get : function(){
		return this._oscillators[0].modulationIndex.value;
	},
	set : function(val){
		for (var i = 0; i < this._oscillators.length; i++){
			this._oscillators[i].modulationIndex.value = val;
		}
	}
});

/**
 *  The harmonicity of the oscillators which make up the source.
 *  see Tone.FMOscillator.harmonicity
 *  @memberOf Tone.MetalSynth#
 *  @type {Positive}
 *  @name  harmonicity
 */
Object.defineProperty(Tone.MetalSynth.prototype, "harmonicity", {
	get : function(){
		return this._oscillators[0].harmonicity.value;
	},
	set : function(val){
		for (var i = 0; i < this._oscillators.length; i++){
			this._oscillators[i].harmonicity.value = val;
		}
	}
});

/**
 *  The frequency of the highpass filter attached to the envelope
 *  @memberOf Tone.MetalSynth#
 *  @type {Frequency}
 *  @name  resonance
 */
Object.defineProperty(Tone.MetalSynth.prototype, "resonance", {
	get : function(){
		return this._filterFreqScaler.min;
	},
	set : function(val){
		this._filterFreqScaler.min = val;
		this.octaves = this._octaves;
	}
});

/**
 *  The number of octaves above the "resonance" frequency
 *  that the filter ramps during the attack/decay envelope
 *  @memberOf Tone.MetalSynth#
 *  @type {Number}
 *  @name  octaves
 */
Object.defineProperty(Tone.MetalSynth.prototype, "octaves", {
	get : function(){
		return this._octaves;
	},
	set : function(octs){
		this._octaves = octs;
		this._filterFreqScaler.max = this._filterFreqScaler.min * Math.pow(2, octs);
	}
});

/**
 *  Clean up
 *  @returns {Tone.MetalSynth} this
 */
Tone.MetalSynth.prototype.dispose = function(){
	Tone.Instrument.prototype.dispose.call(this);
	for (var i = 0; i < this._oscillators.length; i++){
		this._oscillators[i].dispose();
		this._freqMultipliers[i].dispose();
	}
	this._oscillators = null;
	this._freqMultipliers = null;
	this.frequency.dispose();
	this.frequency = null;
	this._filterFreqScaler.dispose();
	this._filterFreqScaler = null;
	this._amplitue.dispose();
	this._amplitue = null;
	this.envelope.dispose();
	this.envelope = null;
	this._highpass.dispose();
	this._highpass = null;
};

export default Tone.MetalSynth;

